/*
 * jaspr.js - Gabe Lerner (gabelerner@gmail.com)
 * MIT Licensed 
 * thanks to http://processingjs.org for some code ...
 */
 
 (function(){
	// initialize by passing in an element and the path to the xml file
	this.jaspr = function jaspr(element, xml) {
		if (typeof element == "string")
			element = document.getElementById(element);

		j = buildJaspr();
		j.init(xml, element);  
		return j;
	};
	
	// log
	function log() {
		try {
			console.log.apply(console, arguments);
		} catch(e) {
			try {
				opera.postError.apply(opera, arguments);
			} catch(e) {}
		}
	}	
	
	// this converts action script syntax into javascript syntax with help from processing.js
	// it keeps track of all classes encountered, and stores them
	var parse = jaspr.parse = function parse(code, j) {
		  // Remove end-of-line comments
		  code = code.replace(/\/\/[^\r\n]+/g, "\n");
		  
		  // Remove multi-line comments
		  code = code.replace(/\/\*[^\*]+\*\//g, "");

		  // Weird parsing errors with %
		  code = code.replace(/([^\s])%([^\s])/g, "$1 % $2");
		 
		  // Simple convert a function-like thing to function
		  code = code.replace(/(?:static )?(\w+ )(\w+)\s*(\([^\)]*\)\s*{)/g, function(all, type, name, args) {
			if ( name == "if" || name == "for" || name == "while" ) {
			  return all;
			} else {
			  return "jaspr." + name + " = function " + name + args;
			}
		  });
			
		  // super() is a reserved word
		  code = code.replace(/super\(/g, "superMethod(");

		  var classes = [];

		  function ClassReplace(all, name, extend, vars, last) {
			classes.push( name );

			var static = "";

			vars = vars.replace(/final\s+var\s+(\w+\s*=\s*.*?;)/g, function(all,set) {
			  static += " " + name + "." + set;
			  return "";
			});

			// Move arguments up from constructor and wrap contents with
			// a with(this), and unwrap constructor
			return "function " + name + "() {with(this){\n  " +
			  (extend ? "var __self=this;function superMethod(){extendClass(__self,arguments," + extend + ");}\n" : "") +
			  // Replace var foo = 0; with this.foo = 0;
			  // and force var foo; to become this.foo = null;
			  vars
				.replace(/,\s?/g, ";\n  this.")
				.replace(/\b(var |final |public )+\s*/g, "this.")
				.replace(/\b(var |final |public )+\s*/g, "this.")
				.replace(/this.(\w+);/g, "this.$1 = null;") + 
				(extend ? "extendClass(this, " + extend + ");\n" : "") +
				"<CLASS " + name + " " + static + ">" + (typeof last == "string" ? last : name + "(");
		  }

		  //var matchClasses = /(?:public |abstract |static )*class (\w+)\s*(?:extends\s*(\w+)\s*)?{\s*((?:.|\n)*?)\b\1\s*\(/g;
		  //var matchNoCon = /(?:public |abstract |static )*class (\w+)\s*(?:extends\s*(\w+)\s*)?{\s*((?:.|\n)*?)(Processing)/g;
		  var matchNoCon = /(?:public |abstract |static )*class (\w+)\s*(?:extends\s*(\w+)\s*)?{\s*((?:[^\n\r]|\r|\n)*?)(jaspr)/g;
		  
		  //code = code.replace(matchClasses, ClassReplace);
		  code = code.replace(matchNoCon, ClassReplace);

		  var matchClass = /<CLASS (\w+) (.*?)>/, m;
		  
		  while ( (m = code.match( matchClass )) ) {
			var left = RegExp.leftContext,
			  allRest = RegExp.rightContext,
			  rest = nextBrace(allRest),
			  className = m[1],
			  staticVars = m[2] || "";
			  
			allRest = allRest.slice( rest.length + 1 );

			rest = rest.replace(new RegExp("\\b" + className + "\\(([^\\)]*?)\\)\\s*{", "g"), function(all, args) {
			  args = args.split(/,\s*?/);
			  
			  if ( args[0].match(/^\s*$/) )
				args.shift();
			  
			  var fn = "if ( arguments.length == " + args.length + " ) {\n";
				
			  for ( var i = 0; i < args.length; i++ ) {
				fn += "    var " + args[i] + " = arguments[" + i + "];\n";
			  }
				
			  return fn;
			});
			
			// Fix class method names
			// this.collide = function() { ... }
			// and add closing } for with(this) ...
			rest = rest.replace(/(?:public )?jaspr.\w+ = function (\w+)\((.*?)\)/g, function(all, name, args) {
			  return "ADDMETHOD(this, '" + name + "', function(" + args + ")";
			});
			
			var matchMethod = /ADDMETHOD([\s\S]*?{)/, mc;
			var methods = "";
			
			while ( (mc = rest.match( matchMethod )) ) {
			  var prev = RegExp.leftContext,
				allNext = RegExp.rightContext,
				next = nextBrace(allNext);

			  methods += "addMethod" + mc[1] + next + "});"
			  
			  rest = prev + allNext.slice( next.length + 1 );
			}

			rest = methods + rest;
			
			code = left + rest + "\n}}" + staticVars + allRest;
		  }

		  // Do some tidying up, where necessary
		  code = code.replace(/jaspr.\w+ = function addMethod/g, "addMethod");
		  
		  function nextBrace( right ) {
			var rest = right;
			var position = 0;
			var leftCount = 1, rightCount = 0;
			
			while ( leftCount != rightCount ) {
			  var nextLeft = rest.indexOf("{");
			  var nextRight = rest.indexOf("}");
			  
			  if ( nextLeft < nextRight && nextLeft != -1 ) {
				leftCount++;
				rest = rest.slice( nextLeft + 1 );
				position += nextLeft + 1;
			  } else {
				rightCount++;
				rest = rest.slice( nextRight + 1 );
				position += nextRight + 1;
			  }
			}
			
			return right.slice(0, position - 1);
		  }

		  function toNumbers( str ){
			var ret = [];
			 str.replace(/(..)/g, function(str){
			  ret.push( parseInt( str, 16 ) );
			});
			return ret;
		  }
		  
		  //this.variable:MovieClip
		  code = code.replace(/this\.(\w+):MovieClip;/g, "this.$1 = getNestedMovieClip(this, \"MovieClip\", \"$1\");");
		  
		  //for(var i in something)
		  code = code.replace(/for\s*\((var )?(\w+) in ([\w\.]+)\)/g,"for (var $2=0;$2<$3.length;$2++)");
	
			// Register Classes
			for (var i=0;i<classes.length;i++) {
				code += "\nclasses.push("+classes[i]+");";
			}	
	
		return code;
	}
	
	function buildJaspr() {
		var j = {};
		
		// ajax helper
		j.ajax=function(url,type){
			if(window.XMLHttpRequest){AJAX=new XMLHttpRequest();}
			else{AJAX=new ActiveXObject("Microsoft.XMLHTTP");}
			if(AJAX){
			   AJAX.open("GET",url,false);
			   AJAX.send(null);
			   return type=='xml' ? AJAX.responseXML : AJAX.responseText;
			}else{return false;}
		}	
		
		// extend class
		j.extendClass = function extendClass( obj, args, fn ) {
			if ( arguments.length == 3 ) {
			  fn.apply( obj, args );
			} else {
			  args.call( obj );
			}
		};

		// add method
		j.addMethod = function addMethod( object, name, fn ) {
			if ( object[ name ] ) {
			  var args = fn.length;
			  
			  var oldfn = object[ name ];
			  object[ name ] = function() {
				if ( arguments.length == args )
				  return fn.apply( this, arguments );
				else
				  return oldfn.apply( this, arguments );
			  };
			} else {
			   object[ name ] = fn;
			}
		};
		
		// main loop, clear and draw every frame
		j.loop = function(framerate) {
			j.loopID = setInterval(function() {
				j.clear();
				j.draw();
			}, 1000 / parseInt(framerate, 10));
		};
		
		// stop the loop
		j.stop = function() {
			clearInterval(j.loopID);
		};
		
		// clear the canvas
		j.clear = function() {
			j.ctx.save();
			j.ctx.fillStyle = '#fff';
			j.ctx.fillRect(0, 0, j.ctx.canvas.width, j.ctx.canvas.height);
			j.ctx.restore();
		};
		
		// to draw, loop through all movies and draw them (todo: need to add z-index sort)
		j.draw = function() {
			for (var m=0; m<j._root.movies.length; m++) {
				j._root.movies[m].draw();
			}
		};

		// takes a root object and extends it to implement the actionscript framework
		j.extendRoot = function(_root, j) {
			_root.depth = 1;
			_root.movies = [];
			
			// implementing actionscript attachMovie
			_root.attachMovie = function(c, id, depth) { 
				var o = j.newMovie(c);
				o._id = id;
				_root.movies.push(o);
				return o;
			}
			
			// implementing actionscript getNextHighestDepth
			_root.getNextHighestDepth = function() { 
				_root.depth += 1;
				return _root.depth;
			}
			
			_root.soundFX = function() { }
			
			// call initialize function
			_root.init();
			j._root = _root;
		};
		
		// defines a actionscript movie clip
		j.MovieClip = function() {
			// actionscript variables
			this._x = this._x || 0;
			this._y = this._y || 0;
			this._width = this._width || 0;
			this._height = this._height || 0;
			this._visible = true;
			this._parent = null;
			this._alpha = 100.0;
			this._rotation = this._rotation || 0;
			
			this.dimensions = function() {
				return { 
					left: this._x - this._width / 2.0,
					top: this._y - this._height / 2.0,
					right: this._x + this._width / 2.0,
					bottom: this._y + this._height / 2.0,
				};
			}
			
			// determines if a hit was made
			// -- can call with hitTest(otherMovie)
			// -- can call with hitTest(x,y)
			this.hitTest = function() {
				if (arguments.length == 1) {
					// test against another object
					var m = arguments[0];
					if (this._removed || m._removed) return false; // make sure they're not removed
					if (this._height == 0 || this._width == 0) return false; // make sure we have dimension
					if (m._height == 0 || m._width == 0) return false; // make sure they have dimension
					
					// simple box hit test
					var d1 = this.dimensions();
					var d2 = m.dimensions();
					if (d1.bottom < d2.top) return false;
					if (d1.top > d2.bottom) return false;
					if (d1.right < d2.left) return false;
					if (d1.left > d2.right) return false;
					return true;
				}
				else {
					var x = arguments[0];
					var y = arguments[1];
					
					// if this movie is drawn in layers, pass through the test there
					if (this.layers) {
						for (var l=0; l<this.layers.length; l++) {
							if (this.layers[l].hitTestP(x,y)) return true;
						}	
						return false;
					}
					else {
						// test for hitting a point
						return this.hitTestP(x,y);
					}
				}
			}
			
			// test for hitting a point
			this.hitTestP = function(x,y) {
				if (this._removed) return false;
				if (this._height == 0 || this._width == 0) return false;
				
				// simple box hit test
				var d1 = this.dimensions();
				if (d1.bottom < y) return false;
				if (d1.top > y) return false;
				if (d1.right < x) return false;
				if (d1.left > x) return false;
				return true;			
			}
			
			// given a local point relative to our object, will transform it to global coordinates
			this.localToGlobal = function(p) {
				var x = this._x; 
				var y = this._y; 
				x += p.y * Math.cos(this._rotation / (360.0 / (2.0 * Math.PI)));
				y += p.y * Math.sin(this._rotation / (360.0 / (2.0 * Math.PI)));
				x += p.x * Math.cos((this._rotation - 90.0) / (360.0 / (2.0 * Math.PI)));
				y += p.x * Math.sin((this._rotation - 90.0) / (360.0 / (2.0 * Math.PI)));
				p.x = x;
				p.y = y;
			}
					
			// implementing actionscript removeMovieClip
			this._removed = false;
			this.removeMovieClip = function() {
				this._removed = true;
				this._visible = false;
				j._root.movies.splice(j._root.movies.indexOf(this), 1);
			}
			
			// draw our movie
			this.draw = function() {				
				if (this._removed || !this._visible) return;
				if (this._parent != null && !this._parent._visible) return; // don't draw us if our parent is invisible
				
				// steal our coordinates from our parent
				if (this._parent != null) {
					this._x = this._parent._x;
					this._y = this._parent._y;
				}				
				
				// fire event for custom logic
				if (this.onEnterFrame) this.onEnterFrame();
				
				// save canvas context
				j.ctx.save();
				
				// set alpha
				if (this._alpha < 100) j.ctx.globalAlpha = this._alpha / 100.0;
				if (this._parent && this._parent._alpha < 100) j.ctx.globalAlpha = this._parent._alpha / 100.0;
				
				// set rotation
				var r = 0.0;
				if (this._rotation != 0) r = this._rotation / (360.0 / (2.0 * Math.PI));
				if (this._parent && this._parent._rotation != 0) r = this._parent._rotation / (360.0 / (2.0 * Math.PI));
				if (r != 0.0) {
					j.ctx.translate(this._x, this._y);
					j.ctx.rotate(r);
					j.ctx.translate(-1.0 * this._x, -1.0 * this._y);
				}
				
				// if we have no width, but we have layers, draw them and set our width from that
				if (this.layers) {		
					var widthFromLayer = this._width == 0 && this._height == 0;
					var width = this._width;
					var height = this._height;
					for (var l=0; l<this.layers.length; l++) {
						var d = this.layers[l].draw(this._x, this._y, width, height);
						if (widthFromLayer) {
							this._width = d.width;
							this._height = d.height;
						}						
					}
				}
				
				// if we can custom draw ourselves, do that
				if (this.customDraw) this.customDraw();
				
				// if the root can custom draw us, do that
				if (j._root.customDraw) j._root.customDraw(this);
				
				// restore canvas context
				j.ctx.restore();
			}		
		};
		
		// implementing actionscript getNestedMovieClip
		j.getNestedMovieClip = function(parent, c, id) {
			var o = j._root.attachMovie(c, id, j._root.getNextHighestDepth());	
			o._parent = parent;
			return o;
		};			
		
		// defines a sound
		j.Sound = function() {
			this.attachSound = function(path) {
				this.sound = path;
			}
			this.start = function(offset, numTimes) {
				if (this.sound) {
					j.playAudio(this.sound);
				}
			}
		}
		
		// attach events
		function attach(elem, type, fn) {
		  if ( elem.addEventListener )
			elem.addEventListener( type, fn, false );
		  else
			elem.attachEvent( "on" + type, fn );
		}		
		
		// actionscript keyboard implementation
		j.Key = new function() {
			this.init = function() {
				j.keys = [];
				attach(document, "keydown", function(e) { j.keys[e.keyCode] = true; });
				attach(document, "keyup", function(e) { j.keys[e.keyCode] = false; });			
			};
			
			this.isDown = function(key) {
				return j.keys[key] == true;
			};
			
			this.UP = 38;
			this.DOWN = 40;
			this.RIGHT = 39;
			this.LEFT = 37;
			this.SPACE = 32;
		};
		
		// actionscript mouse implementation
		j.Mouse = new function() {
			this.x = 0;
			this.y = 0;
			
			this.init = function() {
				attach( j.canvas, "mousemove", function(e) {
				  var scrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
				  var scrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;            
				  j.Mouse.x = e.clientX - j.canvas.offsetLeft + scrollX;
				  j.Mouse.y = e.clientY - j.canvas.offsetTop + scrollY;     
				});
				attach( j.canvas, "mousedown", function(e) {
				  j.Mouse.click();
				});				
			}
			
			// if we click the mouse, and we hit a movie clip, and it knows how to respond to onPress, do it
			this.click = function() { 
				for (var i=0; i<j._root.movies.length; i++) {
					if (j._root.movies[i].onPress && j._root.movies[i].hitTestP(this.x, this.y)) {
						j._root.movies[i].onPress();
					}
				};
			};
		};
		
		// create a canvas
		j.createCanvas = function(element, width, height) {
			j.canvas = document.createElement('canvas');
			j.canvas.setAttribute('width', width);
			j.canvas.setAttribute('height', height);
			j.ctx = j.canvas.getContext('2d');
			element.appendChild(j.canvas);
		};
		
		// store our assets (images, sounds, etc.)
		j.assets = [];
		j.Asset = function(id, type, path) {
			this.id = id;
			this.type = type;
			if (this.type == 'image') {
				this.image = new Image();
				this.image.src = path;
			}
			if (this.type == 'audio') {
				this.audio = document.createElement('audio');
                this.audio.setAttribute('src', path);
			}
		}
		j.createAsset = function(id, type, path) {
			j.assets.push(new j.Asset(id, type, path));
		};
		j.getImage = function(id) {
			for (var i=0; i<j.assets.length; i++) {
				if (j.assets[i].id == id) {
					return j.assets[i].image;
				}
			}
			return null;
		};
		j.playAudio = function(id) {
			for (var i=0; i<j.assets.length; i++) {
				if (j.assets[i].id == id) {
					// todo: figure out how to play sound!
					//log(j.assets[i].audio.src);
					//j.assets[i].audio.play();
					//var oldAudio = j.assets[i].audio;
					//j.assets[i].audio = oldAudio.cloneNode();
				}
			}
		};		
		
		// defines an image layer of a movie
		j.Layer = function(image, x, y) {
			this.image = image;
			this.x = x || 0;
			this.y = y || 0;
			
			// to draw us, draw the image asset
			this.draw = function(offX, offY, width, height) {
				if (this.image == null) return;
				if (width == 0) width = this.image.width;
				if (height == 0) height = this.image.height;
				j.ctx.drawImage(this.image, offX + this.x - width / 2.0, offY + this.y - height / 2.0, width, height);
				return { width: width, height: height };
			}
			
			// store our pixels for hit testing
			if (this.image != null) {
				var canvas = document.createElement('canvas');
				canvas.width = this.image.width;
				canvas.height = this.image.height;
				var ctx = canvas.getContext('2d');
				ctx.drawImage(this.image, 0, 0);
				this.pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
			}
			
			// to test if we're hitting a layer, for now just check against a pixel being non transparent
			this.hitTestP = function(x, y) {
				var off = 4*(this.image.width*Math.floor(y)+Math.floor(x));				
				var r = this.pixels.data[off];
				var g = this.pixels.data[off+1];
				var b = this.pixels.data[off+2];
				var a = this.pixels.data[off+3];
				return a == 255;
			}
		}
		
		// create a new layer
		j.createLayer = function(image, x, y) {
			return new j.Layer(image, x, y);
		}
		
		// defines an empty, codeless movie clip
		j.EmptyClassCode = function(name) {
			return "class " + name + " extends MovieClip" + "\r" +
				   "{" + "\r" +
				   "function onLoad() { }" + "\r" +
				   "}";
		}

		// extend our classes
		j.classes = [];
		j.classes.push(function DynamicText() {
			with(j){
				var __self=this;
				function superMethod(){extendClass(__self,arguments,MovieClip);}
				
				// default text styles
				this.text = '';
				this.fillStyle = '#000';
				this.strokeStyle = '#000';
				this.font = 'normal 12px sans-serif';
				this.type = 'fill';
				
				extendClass(this, MovieClip);
				
				addMethod(this, 'customDraw', function()
				{
					// draw text if we have some
					j.ctx.save();
					j.ctx.textAlign = 'center';
					j.ctx.textBaseline = 'middle';
					j.ctx.font = this.font;
					
					if (this.type == 'fill') {
						j.ctx.fillStyle = this.fillStyle;
						j.ctx.fillText(this.text, this._x, this._y);
					}
					if (this.type == 'stroke') {
						j.ctx.lineWidth = 1;
						j.ctx.strokeStyle = this.strokeStyle;
						j.ctx.strokeText(this.text, this._x, this._y);
					}
					
					j.ctx.restore();
				});
			}
		});
		
		// create a new class by parsing the converted actionscript code
		j.createClass = function(name, code) {
			if (code == '') code = j.EmptyClassCode(name);
			(function(jaspr){with (j){  
				eval(parse(code, j));
			}})(j);  		
			return j.classes[j.classes.length-1];
		};
		
		// initialize jaspr
		j.init = function init(xml, element){  
			var result = j.ajax(xml, 'xml');
			
			// load assets
			var assets = result.documentElement.getElementsByTagName('asset');
			for (var i=0; i<assets.length; i++)
			{
				var id = assets[i].getAttribute('id');
				var type = assets[i].getAttribute('type');
				var path = assets[i].getAttribute('path');
				j.createAsset(id, type, path);
			}
			
			// load classes
			var classes = result.documentElement.getElementsByTagName('clip');
			for (var i=0; i<classes.length; i++)
			{
				var name = classes[i].getAttribute('class');
				var code = '';
				if (classes[i].getAttribute('path') != null) 
				{
					code = j.ajax(classes[i].getAttribute('path'));
				}
				var c = j.createClass(name, code);
				
				var x = parseInt(classes[i].getAttribute('x'), 10);
				if (!isNaN(x)) c.prototype._x = x;
				var y = parseInt(classes[i].getAttribute('y'), 10);
				if (!isNaN(y)) c.prototype._y = y;
				var width = parseInt(classes[i].getAttribute('width'), 10);
				if (!isNaN(width)) c.prototype._width = width;
				var height = parseInt(classes[i].getAttribute('height'), 10);
				if (!isNaN(height)) c.prototype._height = height;
				var rotation = parseInt(classes[i].getAttribute('rotation'), 10);
				if (!isNaN(rotation)) c.prototype._rotation = rotation;				
				
				var layers = classes[i].getElementsByTagName('layer');
				var l = [];
				for (var k=0; k<layers.length; k++)
				{
					var asset = layers[k].getAttribute('asset');
					var x = parseInt(layers[k].getAttribute('x'), 10);
					var y = parseInt(layers[k].getAttribute('y'), 10);
					if (isNaN(x)) x = 0;
					if (isNaN(y)) y = 0;
					l.push(j.createLayer(j.getImage(asset), x, y));
				}
				c.prototype.layers = l;
			} 
			
			// create root
			j._root = j.newMovie('_root');
			j.extendRoot(j._root, j);
			
			// create canvas
			j.createCanvas(element, 
				result.documentElement.getAttribute('width'), 
				result.documentElement.getAttribute('height'));
			
			// initialize keyboard
			j.Key.init();
			
			// initialize mouse
			j.Mouse.init();
				
			// start our main loop at our framerate
			j.loop(result.documentElement.getAttribute('framerate'));
		};
		
		// get class helper
		j.getClass = function(name) {
			for (var c=0; c<j.classes.length; c++) {
				if (j.classes[c].name == name) {
					return j.classes[c];
				}
			}
			return null;
		};
		
		// new movie helper
		j.newMovie = function(name) {
			var o = null;
			var c = j.getClass(name);
			if (c != null) {
				o = new c();
			}
			else {
				o = new j.MovieClip();
			}
			o._name = name;
			if (o.onLoad) o.onLoad();
			return o;
		};
		
		return j;
	}
 })();